package raft

import (
	"encoding/binary"
	"fmt"
	"github.com/hashicorp/raft"
	"go.etcd.io/bbolt"
	"io"
	"os"
	"sync"
	"time"
)

const DBFileName = "kv.db"

var BBoltBucket = []byte("default")

type FSM struct {
	lock sync.Mutex
	db   *bbolt.DB
	dir  string
}

func newFSM(dir string) (*FSM, error) {
	if dir[len(dir)-1] != '/' {
		dir = dir + "/"
	}
	err := ensureDir(dir)
	if err != nil {
		return nil, err
	}
	path := dir + DBFileName
	db, err := bbolt.Open(path, 0600, &bbolt.Options{
		Timeout: 1 * time.Second,
	})
	if err != nil {
		return nil, err
	}
	fsm := &FSM{
		dir: dir,
		db:  db,
	}
	return fsm, nil
}

func raftLogToCommand(log *raft.Log) (CommandType, KVCommand, error) {
	t, cmd := unmarshalRaftLog(log.Data)
	return t, cmd, nil
}

var keyCache = make([]byte, 8)
var valueCache = make([]byte, 8)

type FSMApplyResult struct {
	Success bool
	Value   int64
	Error   error
}

func (this *FSM) Apply(log *raft.Log) interface{} {
	result := &FSMApplyResult{
		Success: false,
	}
	t, cmd, err := raftLogToCommand(log)
	if err != nil {
		result.Error = err
		return result
	}
	binary.LittleEndian.PutUint64(keyCache, uint64(cmd.Key))
	binary.LittleEndian.PutUint64(valueCache, uint64(cmd.Value))
	switch t {
	case CommandPut:
		result.Success, result.Error = this.add(keyCache, valueCache)
	case CommandDelete:
		result.Success, result.Error = this.delete(keyCache)
	case CommandGet:
		result.Value, result.Error = this.get(keyCache)
	case CommandInc:
		result.Value, result.Error = this.inc(keyCache, cmd.Value)
	}
	return result
}

func (this *FSM) add(key, value []byte) (bool, error) {
	success := false
	err := this.db.Update(func(tx *bbolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists(BBoltBucket)
		if err != nil {
			return err
		}
		err = b.Put(key, value)
		if err == nil {
			success = true
		}
		return err
	})
	return success, err
}

func (this *FSM) delete(key []byte) (bool, error) {
	success := false
	err := this.db.Update(func(tx *bbolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists(BBoltBucket)
		if err != nil {
			return err
		}
		err = b.Delete(key)
		if err == nil {
			success = true
		}
		return err
	})
	return success, err

}

func (this *FSM) get(key []byte) (int64, error) {
	var result []byte
	err := this.db.Update(func(tx *bbolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists(BBoltBucket)
		if err != nil {
			return err
		}
		value := b.Get(key)
		result = append(result, value...)
		return err
	})
	if err == nil {
		if result != nil {
			return int64(binary.LittleEndian.Uint64(result)), nil
		} else {
			return 0, fmt.Errorf("key not found")
		}
	}
	return -1, err
}

func (this *FSM) inc(key []byte, add int64) (int64, error) {
	var value int64 = 0
	err := this.db.Update(func(tx *bbolt.Tx) error {
		b, err := tx.CreateBucketIfNotExists(BBoltBucket)
		if err != nil {
			return err
		}
		valueBytes := b.Get(key)
		if len(valueBytes) != 8 {
			logging.Errorf("FSM.inc, key:%d, value length:%d, Reset",
				int64(binary.LittleEndian.Uint64(key)), len(valueBytes))
			valueBytes = make([]byte, 8)
		}
		value = int64(binary.LittleEndian.Uint64(valueBytes))
		value += add
		binary.LittleEndian.PutUint64(valueBytes, uint64(value))
		err = b.Put(key, valueBytes)
		return err
	})
	if err != nil {
		return -1, err
	}
	return value, err
}

func (this *FSM) Snapshot() (raft.FSMSnapshot, error) {
	return &snapshot{
		db: this.db,
	}, nil
}

func (this *FSM) Restore(old io.ReadCloser) error {
	defer old.Close()
	this.lock.Lock()
	defer this.lock.Unlock()
	this.db.Close()
	os.Remove(this.dir)
	ensureDir(this.dir)

	err := copyFile(old, this.dir+DBFileName)
	if err != nil {
		return err
	}
	path := this.dir + DBFileName
	db, err := bbolt.Open(path, 0600, &bbolt.Options{
		Timeout: 1 * time.Second,
	})
	this.db = db
	return err
}
